HTTP 是浏览器中最重要且使⽤最多的协议,是浏览器和服务器之间的通信语⾔,也是互联⽹的基⽯。随着浏览器的发展,HTTP 为了能适应新的形式也在持续进化。
于 1991 年提出的,主要⽤于学术交流,需求很简单⸺⽤来在⽹络之间传递 HTML 超⽂本的内容。
整体来看,它的实现也很简单,采⽤基于请求响应的模式,从客⼾端发出请求,服务器返回数据。
⼀个完整的请求流程:
GET /index.html ⽤来获取 index.htmlHTTP/0.9 的需求就是⽤来传输体积很⼩的 HTML ⽂件,所以有以下三个特点:
1994 年底出现了拨号上⽹服务,同年⽹景推出⼀款浏览器,从此万维⽹就不局限于学术交流了,⽽是进⼊了⾼速的发展阶段。万维⽹的⾼速发展带来了很多新的需求,⽽ HTTP/0.9 已经不能适⽤新兴⽹络的发展,所以这时就需要⼀个新的协议来⽀撑新兴⽹络,这就是 HTTP/1.0 诞⽣的原因。
核⼼诉求:⽀持多种类型的⽂件传输
在浏览器中展⽰的不单是 HTML⽂件,还包括了 JavaScript、CSS、图⽚、⾳频、视频等不同类型的⽂件,⽽且⽂件格式不仅仅局限于 ASCII 编码,还有很多其他类型编码的⽂件
如何实现:
HTTP/1.0 的其他特性(都是通过请求头和响应头来实现的):
HTTP/1.0 为每个请求单独新开一个 TCP 连接。每一个 HTTP 请求-响应,都要先建立 TCP 连接(三次握手),然后完成请求-响应后,再销毁连接(四次挥手)。这就导致每次请求-响应都是相互独立的,无法保持状态。
由于每个请求都是独立的连接,因此会带来下面的问题:
实际上,在 HTTP/1.0 后期,虽然没有官方标准,但开发者们慢慢形成了一个共识:只要请求头中包含 Connection: keep-alive,就表示客户端希望开启长连接,希望服务器响应后不要关闭 TCP 连接。如果服务器认可这一行为,即可保持 TCP 连接。
![[20211027133404.png|500]]
为了解决 HTTP/1.0 的问题,HTTP/1.1 默认开启长连接,即让同一个 TCP 连接服务于多个请求-响应。
在这种情况下,多次请求响应可以共享同一个 TCP 连接,这不仅减少了 TCP 的握手和挥手时间,同时可以充分利用 TCP「慢启动」的特点,有效的利用带宽。
当需要的时候,任何一方都可以关闭 TCP 连接。
连接关闭的情况主要有三种:
Connection:close,服务器收到此请求后,响应结束立即关闭 TCP持久连接虽然能减少 TCP 的建⽴和断开次数,但是它需要等待前⾯的请求返回之后,才能进⾏下⼀次请求。
如果 TCP 通道中的某个请求因某些原因没有及时返回,就会阻塞后⾯的所有请求。
队头阻塞虽然发生在服务器,但这个问题的根源是客户端无法知晓服务器的响应是针对哪个请求的。
正是由于存在队头阻塞,我们常常使用下面的手段进行优化:
浏览器会根据情况,为打开的页面自动开启 TCP 连接,对于同一个域名的连接最多 6 个,如果要突破这个限制,就需要把资源放到不同的域中。
HTTP/1.1 中的管线化是指将多个 HTTP 请求整批提交给服务器的技术,虽然可以整批发送请求,不过服务器依然需要根据请求顺序来回复浏览器的请求
然而,管道化并非一个成功的模型,它带来的队头阻塞造成非常多的问题,FireFox、Chrome 都做过管线化的试验,但是由于各种原因,最终都放弃了管线化技术
![[20211026175005.png|500]]
新增 cache-control、etag 等消息头,优化缓存机制,详见 [[010.HTTP 缓存协议|HTTP 缓存协议]]
在上传/下载资源时,如果资源过大,将其分割为多个部分,分别上传/下载,如果遇到网络故障,可以从已经上传/下载好的地方继续请求,不用从头开始,提高效率。详见 [[009.文件下载、上传、断点续传|断点续传]]
在 HTTP/1.0,每个域名绑定⼀个唯⼀的 IP 地址,⼀个服务器只能⽀持⼀个域名。但是随着虚拟主机技术的发展,需要实现在⼀台物理主机上绑定多个虚拟主机,每个虚拟主机都有⾃⼰的单独的域名,这些单独的域名都公⽤同⼀个 IP 地址
因此,HTTP/1.1 的请求头中增加了 Host 字段,⽤来表⽰当前的域名地址,这样服务器就可以根据不同的 Host 值做不同的处理
在 HTTP/1.0,需要在响应头设置完整的数据⼤⼩,如 Content-Length: 901,这样浏览器就可以根据设置的数据⼤⼩来接收数据。不过随着服务器端的技术发展,很多⻚⾯的内容都是动态⽣成的,因此在传输数据之前并不知道最终的数据⼤⼩,这就导致了浏览器不知道何时会接收完所有的⽂件数据
HTTP/1.1 通过引⼊ Chunk transfer 机制来解决这个问题,服务器会将数据分割成若⼲个任意⼤⼩的数据块,每个数据块发送时会附上上个数据块的⻓度,最后使⽤⼀个零⻓度的块作为发送数据完成的标志。这样就提供了对动态内容的⽀持
详见 [[003.cookie 与 session|cookie]]
HTTP/1.1 的主要问题:对带宽的利⽤率并不理想
⼀旦⼀个 TCP 连接建⽴后,就进⼊了发送数据状态,刚开始 TCP 协议会采⽤⼀个⾮常慢的速度去发送数据,然后慢慢加快发送数据的速度,直到发送数据的速度达到⼀个理想状态
慢启动是 TCP 为了减少⽹络拥塞的⼀种策略:拥塞控制,判断当前网络环境状态
之所以说慢启动会带来性能问题,是因为⻚⾯中常⽤的⼀些关键资源⽂件本来就不⼤,如 HTML⽂件、CSS ⽂件和 JavaScript ⽂件,通常这些⽂件在 TCP 连接建⽴好之后就要发起请求的,但这个过程是慢启动,所以耗费的时间⽐正常的时间要多很多,这样就推迟了宝贵的⾸次渲染⻚⾯的时⻓
同时开启多条 TCP 连接,这些连接会竞争固定的带宽。带宽不⾜时,各个 TCP 连接就需要动态减慢接收数据的速度
有的 TCP 连接下载的是⼀些关键资源,如 CSS ⽂件、JS ⽂件等,⽽有的 TCP 连接下载的是图⽚、视频等普通的资源⽂件,但是多条 TCP 连接之间不能协商让哪些关键资源优先下载,这样可能影响那些关键资源的下载速度
在 HTTP/1.1 使⽤持久连接时,虽然能公⽤⼀个 TCP 管道,但是在⼀个管道中同⼀时刻只能处理⼀个请求,在当前的请求没有结束之前,其他的请求只能处于阻塞状态
其中慢启动和 TCP 连接之间相互竞争带宽是由于 TCP 本⾝的机制导致的,队头阻塞是由于 HTTP/1.1 的机制导致的
解决 HTTP/1.1 对带宽的利⽤率不理想的问题
⼀个域名只使⽤⼀个 TCP ⻓连接:
实现资源的并⾏请求:解决了应⽤层⾯的队头阻塞问题
每个请求都有⼀个对应的 ID,浏览器可以随时将请求发送给服务器,服务器也可以随意返回内容(浏览器接收到之后,会筛选出相同 ID 的内容,将其拼接为完整的 HTTP 响应数据)
HTTP/2 使⽤了多路复⽤技术,可以将请求分成⼀帧⼀帧的数据去传输,这样带来了⼀个额外的好处,就是当收到⼀个优先级⾼的请求时,服务器可以暂停之前的请求来优先处理关键资源的请求
HTTP/2 允许以更小的单元传输数据,每个传输单元称为帧,而每一个请求或响应的完整数据称为流,每个流有自己的编号,每个帧会记录所属的流(每个帧都带了一个头部,记录了流的 ID)。
引⼊⼆进制分帧层,就实现了 HTTP 的多路复⽤技术:
![[⼆进制分帧层.png]]
HTTP/2 的请求和接收过程:
都是基于⼆进制分帧层
在发送请求时,可以标上该请求的优先级,这样服务器接收到请求后,会优先处理优先级⾼的请求
当⽤⼾请求⼀个⻚⾯后,服务器知道该⻚⾯会引⽤⼏个重要的⽂件,那么在接收到⻚⾯请求后,附带将要使⽤的⽂件⼀并发送给浏览器,这样当浏览器解析完页面之后,就能直接拿到需要⽂件,这对⾸次打开⻚⾯的速度起到了⾄关重要的作⽤
HPACK 算法,静态表和动态表
HTTP/2 之前,所有的消息头都是以字符的形式完整传输的。可实际上,大部分头部信息都有很多的重复。为了解决这一问题,HTTP/2 使用头部压缩来减少消息头的体积
![[20211027132744.png]]
对于两张表都没有的头部,则使用 Huffman 编码压缩后进行传输,同时添加到动态表中
HTTP/2 的语义和 HTTP/1.1 依然是⼀样的,也就是说它们通信的语⾔并没有改变,开发者依然可以通过 Accept 请求头告诉服务器希望接收到什么类型的⽂件,依然可以使⽤ Cookie 来保持登录状态,依然可以使⽤ Cache 来缓存本地⽂件,这些都没有变,发⽣改变的只是传输⽅式。这⼀点对开发者来说尤为重要,这意味着我们不需要为 HTTP/2 去重建⽣态,并且 HTTP/2 推⼴起来会也相对更轻松
在 TCP 传输过程中,由于单个数据包的丢失⽽造成的阻塞
虽然 HTTP/2 解决了应⽤层⾯的队头阻塞问题,不过和 HTTP/1.1 ⼀样,HTTP/2 依然是基于 TCP 协议的,⽽ TCP 最初就是为了单连接⽽设计的。可以把 TCP 连接看成是两台计算机之前的⼀个虚拟管道,计算机的⼀端将要传输的数据按照顺序放⼊管道,最终数据会以相同的顺序出现在管道的另外⼀头
如果在数据传输的过程中,有⼀个数据因为⽹络故障或其他原因⽽丢包了,那么整个 TCP 的连接就会处于暂停状态,需要等待丢失的数据包被重新传输过来
HTTP/2 VS HTTP/1.1:
在 HTTP/2 中,多个请求是跑在⼀个 TCP 管道中的,如果其中任意⼀路数据流中出现了丢包的情况,那么就会阻塞该 TCP 连接中的所有请求
这不同于 HTTP/1.1,使⽤ HTTP/1.1 时,浏览器为每个域开启了 6 个 TCP 连接,如果其中的 1 个 TCP 连接发⽣了队头阻塞,那么其他的 5 个连接依然可以继续传输数据
所以随着丢包率的增加,HTTP/2 的传输效率也会越来越差。有测试数据表明,当系统达到了 2% 的丢包率时,HTTP/1.1 的传输效率反⽽⽐ HTTP/2 表现得更好
除了 TCP 队头阻塞之外,TCP 的握⼿过程也是影响传输效率的⼀个重要因素
⽹络延迟 RTT(Round Trip Time):从浏览器发送⼀个数据包到服务器,再从服务器返回数据包到浏览器的整个往返时间,RTT 是反映⽹络性能的⼀个重要指标
建⽴TCP 连接需要花费多少个 RTT?
HTTP/1 和 HTTP/2 都是使⽤TCP 协议来传输的,⽽如果使⽤ HTTPS 的话,还需要使⽤ TLS 协议进⾏安全传输,⽽使⽤ TLS 也需要⼀个握⼿过程,这样就需要有两个握⼿延迟过程:
总之,在传输数据之前,需要花掉 3〜4 个 RTT
是否可以通过改进 TCP 协议来解决上述的这些问题呢?答案是:⾮常困难
协议有了新的特征,而在中间设备引入(了解)这些新特性之前,它们会认为这种特征的数据包是非法的、恶意的,于是会将这种流量直接扔掉,或是拖延到用户不再想使用这些新特征的程度
这种问题就被称之为“协议僵化”,协议僵化影响了 TCP 协议的改变
尽可能将通信加密是对抗僵化的唯一有效手段,加密可以防止中间设备看到协议传输的绝大部分内容
HTTP/2 存在⼀些⽐较严重的与 TCP 协议相关的缺陷,但由于 TCP 协议僵化,⼏乎不可能通过修改 TCP 协议⾃⾝来解决这些问题,那么解决问题的思路是绕过 TCP 协议,发明⼀个 TCP 和 UDP 之外的新的传输协议
但是这也⾯临着和修改 TCP⼀样的挑战,因为中间设备的僵化,这些设备只认 TCP 和 UDP,如果采⽤了新的协议,新协议在这些设备同样不被很好地⽀持
因此,HTTP/3 选择了⼀个折衷的⽅法⸺UDP 协议,基于 UDP 实现了类似于 TCP 的多路数据流、传输可靠性等功能,我们把这套功能称为 QUIC 协议
HTTP/3 中的 QUIC 协议集合了以下⼏点功能:
![[QUIC.png]]